cover

Flask 中的文件上传和下载

本文主要介绍了在 flask 中如何实现文件的上传和下载, 同时稍微深入的探寻了一下文件上传和下载的底层原理.

很久以前

概述

在 flask 中实现文件上传, 需要使用的是 request.files对象, 基本流程是:

  1. 在模板的定义 enctype=multipart/form-dataform
  2. form 中定义 input[type=file, name=filename]
  3. 使用 request.files[filename] 来获取上传的文件 FileStorage 对象
  4. FileStorage 对象中, filename 为文件名, 使用 read 方法可以读取文件内容

在 flask 中实现文件下载, 需要使用 send_file 方法, 基本流程是:

  1. 构造/获取 需要被下载文件的 file pointer
  2. send_file(fp, as_attachment=True, attachment_filename=filename)作为返回响应
import tempfilefrom flask 
import requests, send_file

def handler(content_raw):
  # handle raw content in file
  return content_raw

def read_and_download():
  file = request.files.values()[0]
  filename, file_content = file.filename, file.read()
  content_handled = handler(file_content) 
  # handler use to handle file content
  tmp = tempfile.TemporaryFile()          
  # construct a tmp file
  tmp.write(content_handled)              
  # save handled content to tmp file
  tmp.seek(0)                             
  # reset file stream position
  return send_file(tmp, as_attachment=True, attachment_filename="download.ext")

文件上传的原理

文件上传, 其本质是通过客户端(仅以浏览器为例)将一种数据传输到服务器(即通过 form 提交数据), 其特殊之处在于, 传输的数据类型是二进制类型数据

一般的情况下, 我们通过 form 提交数据, 要做的事情有两件:

  1. 定义提交的目的地, 通过指定 form 的 action 实现
  2. 定义提交的方法, 通过指定 form 的 method 实现

当服务器接收到来自浏览器的请求之后, 会根据请求头的内容对数据进行处理.

对于一般的 form 数据, 浏览器会默认为请求头添加 Content-Type: application/x-www-form-urlencoded (其含义为传输的数据类型是经过 URL 编码的数据).

但是对于文件类型的数据我们必须要手动设置 Content-Type(因为文件的类型是二进制数据), 因此, 对于文件上传, 我们需要针对提交 form 数据添加额外的参数:

  • 设置 formenctypemultipart/form-data

设置之后, 请求头中会出现 Content-Type: multipart/form-data, multipart/form-data 这一类型的含义是被分割为多份的表单数据.

通过这样的处理, 文件就能被服务器接收到并且进行正确的处理, 下面是一个基于 flask 的文件上传的请求头实例

header

在 flask 中, 对于 上传的文件数据 的获取主要是在 werkzeug 中实现的, 想要阅读源码(flask==1.0.2)可以参考以下路径:

  1. flask/wrappers.py[L:168] - Request._load_form_data
  2. werkzeug/wrappers.py[L:379 ~ L:385] - parser.parse
  3. werkzeug/formparser.py[L:213] - _parse_multipart

文件下载的原理

这里所说的文件下载并不是只将 flask 作为 client 进行下载, 而是指访问者可以从 flask server 下载文件

其实, 文件下载的本质是客户端(以浏览器为例)针对服务器响应的特殊处理.

我们看到的网页内容本质上都是服务器返回给浏览器的数据, 这些数据之所以会以不同的形式出现, 是因为浏览器会根据响应头中的 Content-Type 决定采用何种方式处理这些数据.

例如, 如果在响应头中存在 Content-Type: text/html, 那么浏览器就会将返回数据以 HTML 的形式进行渲染.

那么一个可以下载文件的响应的请求头回事什么样的呢?

response

上图是一个简单的基于 flask 实现的文件下载的响应头, 图中红框标注的是要重点关注的地方.

Content-Type: application/octet-stream 表示响应数据的类型是 通用的二进制 类型数据, 浏览器针对 通用二进制 类型数据的默认处理方法就是触发 下载 操作.

Content-Disposition: attachment; filename="filename.ext" 的含义是, 以附件形式下载, 并且下载文件名为 filename.ext.

设置 Content-Disposition 头的原因有二:

  1. 某些类型的二进制数据浏览器是可用进行渲染的(比如 json 或者 pdf), 因此浏览器的默认行为就不再是下载而是渲染(此时 Content-Disposition 的值为 inline, 因此对于如果希望总是触发下载操作, 就需要手动设置 Content-Dispositionattachment (即下载).
  2. 默认的情况下, 浏览器触发的下载操作并不会对下载的文件进行命名, 因此需要手动进行命名, 即为 Content-Disposition 添加 filename="filename".

Flask 中实现文件上传和下载的方法

在 flask 中实现文件的上传与下载并不复杂, 因为许多针对底层的操作 flask 已经封装好了.

实现文件上传

flask 中实现文件上传的步骤主要有四:

  1. 在 template 中定义 enctypemultipart/form-data , methodPOST 的 form 元素
  2. 在 form 添加 input[type=file] 元素
  3. 在 views 中定义处理 form action 的方法
  4. 在 view function 中 使用 request.files 获取文件并进行处理

下面是一个简单的例子(摘自 flask 官方文档并进行了一定的修改, 直接保存为 app.py 并运行即可)

from flask import request, Flask, redirect

def upload_file():
  if request.method == 'POST':
    # check if the post request has the file part
    if 'file' not in request.files:
      print('No file part')
      return redirect(request.url)

    file = request.files['file']
    # if user does not select file, browser also
    # submit an empty part without filename
    if file.filename == '':
       print('No selected file')
       return redirect(request.url)

    return file.read().decode() # convert to str
  return '''
<!doctype html>人
  <title>Upload new File</title>
  <h1>Upload new File</h1>
  <form action="{{ url_for('upload') }}" method="POST" enctype=multipart/form-data>
    <input type="file" name="file">
    <input type="submit" value="Upload">
  </form>
</html>
'''

app = Flask(__file__)
app.add_url_rule('', 'index', upload_file, methods=('GET',))
app.add_url_rule('', 'download', upload_file, methods=('POST',))
app.run(debug=True)

flask.requests 是一个 MultiDict(即一个 key 可以对应多个 value), 通过在定义 input[type=file, name=name] 即可以通过 flask.requests[name] 的方式获取到上传的文件.

上传的文件是 FileStorage 的实例, FileStoragewerkzeug 针对 request stream 的封装, 所以可以直接使用 request stream 的方法进行处理.

比如, 获取文件名: FileStorage().filename, 获取文件大小: FileStorage().content_length, 获取文件内容: FileStorage().read()

注意

  • FileStoragestream 的封装, 因此在 read 之后 pointer 就指向了 stream 的末尾, 此时再次 read 无法得到任何内容, 如果需要再次使用 read 方法获取内容, 需要先 file.seek(0) 将 pointer 重新指向 stream 头部

实现文件下载

实现文件下载主要是利用 flask 提供的 send_file, 一个简单的例子如下(直接保存为 app.py 并运行即可):

import json
import tempfile
from flask import Flask, send_file

def download_file():
  file = tempfile.TemporaryFile()
  json.dump({'name': 'demo', 'age': 22}, file)
  return send_file(file, as_attachment=True, attachment_filename='demo.json')

app = Flask(__file__)

app.add_url_rule('', 'download', download_file, methods=('GET,'))

app.run(debug=True)

如过希望下载文件, 则必须设置 as_attachment=True 以及 attachment_filename=filename.

需要注意的是, send_file 的第一个参数要求必须是 file pointer(后续简称 fp). 一般地, 在 python 中得到一个 fp 的方式是使用 open 方法打开一个文件, 但这就要求文件必须存储于服务器上, 在某些情况下这样子是可行的, 但是在一般情况下, 服务器并不需要保存生成的文件, 在文件被下载之后就可以移除, 这样子能减少服务器在存储上的负担.

为此, 可以使用临时文件解决这个问题, 使用 python 标准库中的 tempfile 可以很方便地生成临时文件, 如果使用 (Named)TemporaryFile 甚至可以不考虑清除临时文件的问题(tempfile 会在文件被关闭后自行清理).

注意:

  1. 不要以 with context(如 with tempfile.Temporary() as wf: …) 的形式使用 tempfile, 因为在这种情况下执行 file.write (或类似操作)之后会自动关闭 fp 导致 send_file 时遇到 的错误(有一点我无法确认, 在 flask send_file 完成之后是否会自动 close file)

    send closed file

  2. 当执行完 file.write (或类似操作)之后, 需要使用 file.seek(0) 重置 stream 位置(原因在上一节最后有说明), 否则下载的文件将会是空文件

实例

Online File Converter 是一个基于 flask 实现的站点, 这个站点的作用是将用户上传的 csv 文件合并后以 excel 的形式下载, 这是一个关于 flask 文件上传和下载的完整例子.

参考资料

  1. 发送表单数据
  2. HTTP MIME types
  3. HTTP Header: Content-Disposition
  4. Flask: FileStorage
  5. Flask: demo of file upload